轻松聘
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

1247 lines
34 KiB

import { Ionicons } from '@expo/vector-icons';
import { useLocalSearchParams, useRouter } from 'expo-router';
import { useEffect, useState } from 'react';
import {
ActivityIndicator,
Alert,
KeyboardAvoidingView,
Modal,
Platform,
Pressable,
ScrollView,
StyleSheet,
Text,
TextInput,
View,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import Svg, { Circle, Line, Polygon, Text as SvgText } from 'react-native-svg';
import { RatingStars, STAR_COLOR } from '../../components/RatingStars';
import { useFavorites, useJobDetail } from '../../src/hooks';
import { buildJobShareText } from '../../src/api';
import {
REVIEW_DIMENSIONS,
REVIEW_TAG_SUGGESTIONS,
} from '../../src/constants';
import { shareText } from '../../src/lib/shareFile';
import {colors, spacing} from '../../src/theme';
import { formatThousands } from '../../src/utils';
import type {
EmployeeReview,
JobDimensionScore,
JobReviewCreateInput,
JobReviewSummary,
} from '../../src/types';
/* ===================== 雷达图(6 维度,SVG 绘制) ===================== */
/** 六边形雷达图:同心网格 + 轴线 + 数据多边形 + 维度标签 */
function RadarChart({
dimensions,
size = 160,
}: {
dimensions: JobDimensionScore[];
size?: number;
}) {
const cx = size / 2;
const cy = size / 2;
const count = dimensions.length;
// 数据/网格最大半径(预留标签空间)
const R = size / 2 - 36;
const labelR = R + 16;
const levels = [0.25, 0.5, 0.75, 1];
const angleFor = (i: number) => (-90 + i * (360 / count)) * (Math.PI / 180);
const pointFor = (i: number, ratio: number) => ({
x: cx + R * ratio * Math.cos(angleFor(i)),
y: cy + R * ratio * Math.sin(angleFor(i)),
});
const polygonPoints = (ratio: number) =>
dimensions
.map((_, i) => {
const p = pointFor(i, ratio);
return `${p.x},${p.y}`;
})
.join(' ');
const dataPoints = dimensions.map((d, i) => pointFor(i, d.value / 100));
return (
<Svg width={size} height={size}>
{/* 同心网格 */}
{levels.map((ratio) => (
<Polygon
key={ratio}
points={polygonPoints(ratio)}
fill="none"
stroke={colors.border}
strokeWidth={1}
/>
))}
{/* 轴线 */}
{dimensions.map((_, i) => {
const p = pointFor(i, 1);
return (
<Line key={i} x1={cx} y1={cy} x2={p.x} y2={p.y} stroke={colors.border} strokeWidth={1} />
);
})}
{/* 数据多边形 */}
<Polygon
points={dataPoints.map((p) => `${p.x},${p.y}`).join(' ')}
fill={colors.primary}
fillOpacity={0.15}
stroke={colors.primary}
strokeWidth={2}
/>
{/* 数据点 */}
{dataPoints.map((p, i) => (
<Circle key={i} cx={p.x} cy={p.y} r={3} fill={colors.primary} />
))}
{/* 维度标签 */}
{dimensions.map((d, i) => {
const lx = cx + labelR * Math.cos(angleFor(i));
const ly = cy + labelR * Math.sin(angleFor(i));
return (
<SvgText
key={d.id}
x={lx}
y={ly}
fontSize={10}
fill={colors.textSecondary}
textAnchor="middle"
alignmentBaseline="middle"
>
{d.name}
</SvgText>
);
})}
</Svg>
);
}
/* ===================== 子组件 ===================== */
/** 顶部导航:返回 + 标题 + 收藏 + 分享(onShare 缺省时不渲染分享按钮,避免留下点了没反应的入口) */
function Header({
onBack,
onShare,
favorited,
onToggleFavorite,
}: {
onBack: () => void;
onShare?: () => void;
favorited?: boolean;
onToggleFavorite?: () => void;
}) {
return (
<View style={styles.header}>
<Pressable style={styles.headerBtn} onPress={onBack} hitSlop={8}>
<Ionicons name="chevron-back" size={24} color={colors.textPrimary} />
</Pressable>
<Text style={styles.headerTitle}>岗位详情</Text>
<View style={{ flexDirection: 'row' }}>
{onToggleFavorite ? (
<Pressable style={styles.headerBtnRight} onPress={onToggleFavorite} hitSlop={8}>
<Ionicons
name={favorited ? 'heart' : 'heart-outline'}
size={22}
color={favorited ? colors.primary : colors.textPrimary}
/>
</Pressable>
) : null}
{onShare ? (
<Pressable style={styles.headerBtnRight} onPress={onShare} hitSlop={8}>
<Ionicons name="share-social-outline" size={22} color={colors.textPrimary} />
</Pressable>
) : null}
</View>
</View>
);
}
/** 分区标题:竖条装饰 + 标题 + 可选右侧链接 */
function SectionTitle({
title,
link,
onLink,
}: {
title: string;
link?: string;
onLink?: () => void;
}) {
return (
<View style={styles.sectionHeader}>
<View style={styles.sectionBar} />
<Text style={styles.sectionTitle}>{title}</Text>
{link ? (
<Pressable style={styles.sectionLinkBtn} onPress={onLink} hitSlop={8}>
<Text style={styles.sectionLink}>{link} &gt;</Text>
</Pressable>
) : null}
</View>
);
}
/* ===================== 评价:汇总 / 卡片 / 编辑器 ===================== */
/**
* 评价汇总卡:平均分 + 星级分布 + 热门标签。
* 无评价时不渲染(空态由下方「暂无员工评价」承担,避免两个空块叠着)。
*/
function ReviewSummaryCard({ summary }: { summary: JobReviewSummary }) {
if (summary.total === 0) return null;
const maxCount = Math.max(1, ...summary.distribution.map((d) => d.count));
return (
<View style={styles.card}>
<View style={styles.summaryTop}>
<View style={styles.summaryScoreCol}>
<Text style={styles.summaryScore}>{summary.averageRating.toFixed(1)}</Text>
<RatingStars value={Math.round(summary.averageRating)} size={13} />
<Text style={styles.summaryTotal}>{summary.total} 条真实评价</Text>
</View>
<View style={styles.summaryBars}>
{summary.distribution.map((d) => (
<View key={d.rating} style={styles.summaryBarRow}>
<Text style={styles.summaryBarLabel}>{d.rating}星</Text>
<View style={styles.summaryBarTrack}>
<View
style={[
styles.summaryBarFill,
{ width: `${Math.round((d.count / maxCount) * 100)}%` },
]}
/>
</View>
<Text style={styles.summaryBarCount}>{d.count}</Text>
</View>
))}
</View>
</View>
{summary.topTags.length > 0 ? (
<View style={styles.reviewTags}>
{summary.topTags.map((tag) => (
<View key={tag} style={styles.reviewTag}>
<Text style={styles.reviewTagText}>{tag}</Text>
</View>
))}
</View>
) : null}
</View>
);
}
/**
* 单条评价卡。
* `canEdit` 为真(本人评价)时展示「编辑 / 删除」;八维评分按行铺开,
* 未打分的维度不渲染,避免一屏全是 0 分占位。
*/
function ReviewCard({
review,
onEdit,
onDelete,
}: {
review: EmployeeReview;
onEdit: () => void;
onDelete: () => void;
}) {
const scoredDims = REVIEW_DIMENSIONS.filter(
(dim) => (review.dimensionScores?.[dim.key] ?? 0) > 0,
);
return (
<View style={styles.card}>
<View style={styles.reviewHeader}>
<Text style={styles.reviewName}>{review.userName}</Text>
<Text style={styles.reviewRole}>{review.userRole}</Text>
<Text style={styles.reviewTime}>{review.time}</Text>
</View>
{typeof review.rating === 'number' && review.rating > 0 ? (
<View style={styles.reviewRatingRow}>
<RatingStars value={review.rating} size={13} />
<Text style={styles.reviewRatingText}>{review.rating}.0</Text>
</View>
) : null}
<Text style={styles.reviewContent}>{review.content}</Text>
{scoredDims.length > 0 ? (
<View style={styles.dimGrid}>
{scoredDims.map((dim) => (
<View key={dim.key} style={styles.dimCell}>
<Text style={styles.dimLabel}>{dim.label}</Text>
<Text style={styles.dimValue}>{review.dimensionScores?.[dim.key]}</Text>
</View>
))}
</View>
) : null}
{review.tags.length > 0 ? (
<View style={styles.reviewTags}>
{review.tags.map((tag) => (
<View key={tag} style={styles.reviewTag}>
<Text style={styles.reviewTagText}>{tag}</Text>
</View>
))}
</View>
) : null}
{review.canEdit ? (
<View style={styles.reviewActions}>
<Pressable style={styles.reviewActionBtn} onPress={onEdit} hitSlop={6}>
<Ionicons name="create-outline" size={14} color={colors.primaryText} />
<Text style={styles.reviewActionText}>编辑</Text>
</Pressable>
<Pressable style={styles.reviewActionBtn} onPress={onDelete} hitSlop={6}>
<Ionicons name="trash-outline" size={14} color={colors.dangerText} />
<Text style={[styles.reviewActionText, styles.reviewActionDanger]}>删除</Text>
</Pressable>
</View>
) : null}
</View>
);
}
/** 编辑器初值:新建时全 0,编辑时用已有评价回填 */
interface ReviewDraft {
rating: number;
content: string;
tags: string[];
dimensionScores: Record<string, number>;
}
const EMPTY_DRAFT: ReviewDraft = { rating: 0, content: '', tags: [], dimensionScores: {} };
function draftFromReview(review: EmployeeReview | null): ReviewDraft {
if (!review) return EMPTY_DRAFT;
return {
rating: review.rating ?? 0,
content: review.content,
tags: [...review.tags],
dimensionScores: { ...(review.dimensionScores ?? {}) },
};
}
/**
* 评价编辑器(新建 / 编辑共用)。
* 后端要求 rating + 八维 + 标签 + 正文一起提交,故不能再沿用底部的单行输入框。
* 校验在提交前做:综合评分必填、正文必填。
*/
function ReviewEditorModal({
visible,
initial,
submitting,
onClose,
onSubmit,
}: {
visible: boolean;
/** 编辑时传入被编辑的评价;新建传 null */
initial: EmployeeReview | null;
submitting: boolean;
onClose: () => void;
onSubmit: (input: JobReviewCreateInput) => void;
}) {
const [draft, setDraft] = useState<ReviewDraft>(EMPTY_DRAFT);
// 每次打开时按 initial 重置,避免上一次的残留内容带进新一次编辑
useEffect(() => {
if (visible) setDraft(draftFromReview(initial));
}, [visible, initial]);
const toggleTag = (tag: string) => {
setDraft((prev) => ({
...prev,
tags: prev.tags.includes(tag)
? prev.tags.filter((t) => t !== tag)
: [...prev.tags, tag],
}));
};
const setDimension = (key: string, score: number) => {
setDraft((prev) => ({
...prev,
dimensionScores: { ...prev.dimensionScores, [key]: score },
}));
};
const handleSubmit = () => {
if (draft.rating <= 0) {
Alert.alert('提示', '请先给一个综合评分');
return;
}
if (!draft.content.trim()) {
Alert.alert('提示', '请先输入你的评价内容');
return;
}
onSubmit({
rating: draft.rating,
content: draft.content.trim(),
tags: draft.tags,
// 未打分的维度补 0,键齐全便于后端按八维落库
dimensionScores: REVIEW_DIMENSIONS.reduce<Record<string, number>>((acc, dim) => {
acc[dim.key] = draft.dimensionScores[dim.key] ?? 0;
return acc;
}, {}),
});
};
return (
<Modal visible={visible} transparent animationType="slide" onRequestClose={onClose}>
<KeyboardAvoidingView
style={styles.modalMask}
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
>
<View style={styles.modalSheet}>
<View style={styles.modalHeader}>
<Text style={styles.modalTitle}>{initial ? '编辑评价' : '写评价'}</Text>
<Pressable onPress={onClose} hitSlop={8} accessibilityLabel="关闭">
<Ionicons name="close" size={22} color={colors.textSecondary} />
</Pressable>
</View>
<ScrollView style={styles.modalBody} keyboardShouldPersistTaps="handled">
<Text style={styles.fieldLabel}>综合评分</Text>
<View style={styles.ratingRow}>
<RatingStars
value={draft.rating}
size={26}
onChange={(next) => setDraft((prev) => ({ ...prev, rating: next }))}
/>
<Text style={styles.ratingHint}>
{draft.rating > 0 ? `${draft.rating} 分` : '点击星星打分'}
</Text>
</View>
<Text style={styles.fieldLabel}>分项评分(选填)</Text>
<View style={styles.dimEditor}>
{REVIEW_DIMENSIONS.map((dim) => (
<View key={dim.key} style={styles.dimEditorRow}>
<Text style={styles.dimEditorLabel}>{dim.label}</Text>
<RatingStars
value={draft.dimensionScores[dim.key] ?? 0}
size={16}
onChange={(next) => setDimension(dim.key, next)}
/>
</View>
))}
</View>
<Text style={styles.fieldLabel}>标签(选填,可多选)</Text>
<View style={styles.tagPicker}>
{REVIEW_TAG_SUGGESTIONS.map((tag) => {
const active = draft.tags.includes(tag);
return (
<Pressable
key={tag}
style={[styles.tagChip, active && styles.tagChipActive]}
onPress={() => toggleTag(tag)}
>
<Text style={[styles.tagChipText, active && styles.tagChipTextActive]}>
{tag}
</Text>
</Pressable>
);
})}
</View>
<Text style={styles.fieldLabel}>评价内容</Text>
<TextInput
style={styles.modalInput}
placeholder="说说这个岗位的真实情况,帮助更多人做判断..."
placeholderTextColor={colors.textHint}
value={draft.content}
onChangeText={(text) => setDraft((prev) => ({ ...prev, content: text }))}
multiline
maxLength={500}
/>
<Text style={styles.counterText}>{draft.content.length}/500</Text>
</ScrollView>
<View style={styles.modalFooter}>
<Pressable
style={[styles.submitBtn, submitting && styles.submitBtnDisabled]}
onPress={handleSubmit}
disabled={submitting}
>
{submitting ? (
<ActivityIndicator size="small" color={colors.white} />
) : (
<Text style={styles.submitText}>{initial ? '保存修改' : '发布评价'}</Text>
)}
</Pressable>
</View>
</View>
</KeyboardAvoidingView>
</Modal>
);
}
/* ===================== 页面 ===================== */
export default function JobDetailScreen() {
const { id } = useLocalSearchParams<{ id: string }>();
const router = useRouter();
const {
detail,
overallScore,
reviews,
reviewSummary,
loading,
error,
refresh,
publishReview,
editReview,
removeReview,
} = useJobDetail(id);
const [editorOpen, setEditorOpen] = useState(false);
/** 正在编辑的评价;null = 新建 */
const [editing, setEditing] = useState<EmployeeReview | null>(null);
const [submitting, setSubmitting] = useState(false);
/**
* 本人已发布的评价。后端约束「同一学生对同一岗位仅一条」,故取第一条 canEdit 即可。
* 有它时底部按钮变「编辑我的评价」,避免用户以为能发第二条。
*/
const myReview = reviews.find((r) => r.canEdit) ?? null;
// 收藏态:用已收藏 id 集合判断是否已收藏(仅 STUDENT 可收藏)
const { favoriteIds, toggle: toggleFavorite } = useFavorites();
const favorited = id ? favoriteIds.has(id) : false;
/**
* 分享岗位:把当前岗位拼成文本经系统分享面板发出。
* `ok` 只代表面板已拉起,不能据此提示「分享成功」,只在失败时说明原因(见 lib/shareFile)。
*/
const handleShare = async () => {
if (!detail) return;
const outcome = await shareText(
buildJobShareText(detail, overallScore, reviews.length),
`${detail.positionName}|${detail.companyName}`,
);
if (!outcome.ok) Alert.alert('无法分享', outcome.message);
};
const openCreate = () => {
setEditing(null);
setEditorOpen(true);
};
const openEdit = (review: EmployeeReview) => {
setEditing(review);
setEditorOpen(true);
};
// 新建 / 编辑共用一个提交口:编辑时忽略岗位覆盖,新建时由 hook 补岗位 id
const handleSubmitReview = async (input: JobReviewCreateInput) => {
setSubmitting(true);
try {
if (editing) {
await editReview(editing.id, input);
} else {
await publishReview(input);
}
setEditorOpen(false);
setEditing(null);
} catch (e) {
Alert.alert(editing ? '保存失败' : '发布失败', (e as Error).message || '请稍后重试');
} finally {
setSubmitting(false);
}
};
// 删除前二次确认:评价删掉不可恢复,不做静默删除
const handleDeleteReview = (review: EmployeeReview) => {
Alert.alert('删除评价', '删除后无法恢复,确定要删除这条评价吗?', [
{ text: '取消', style: 'cancel' },
{
text: '删除',
style: 'destructive',
onPress: async () => {
try {
await removeReview(review.id);
} catch (e) {
Alert.alert('删除失败', (e as Error).message || '请稍后重试');
}
},
},
]);
};
// 错误态
if (error) {
return (
<SafeAreaView style={styles.safeArea} edges={['top', 'bottom']}>
<Header onBack={() => router.back()} favorited={false} onToggleFavorite={() => {}} />
<View style={styles.stateWrap}>
<Text style={styles.stateText}>加载失败,请重试</Text>
<Pressable style={styles.retryBtn} onPress={refresh}>
<Text style={styles.retryText}>重试</Text>
</Pressable>
</View>
</SafeAreaView>
);
}
// 加载态
if (loading || !detail || !overallScore) {
return (
<SafeAreaView style={styles.safeArea} edges={['top', 'bottom']}>
<Header onBack={() => router.back()} favorited={false} onToggleFavorite={() => {}} />
<View style={styles.stateWrap}>
<ActivityIndicator size="large" color={colors.primary} />
</View>
</SafeAreaView>
);
}
return (
<SafeAreaView style={styles.safeArea} edges={['top', 'bottom']}>
<Header
onBack={() => router.back()}
onShare={handleShare}
favorited={favorited}
onToggleFavorite={id ? () => toggleFavorite(id) : undefined}
/>
<ScrollView
style={styles.scroll}
contentContainerStyle={styles.content}
showsVerticalScrollIndicator={false}
>
{/* 1. 岗位基本信息 */}
<View style={styles.card}>
<View style={styles.basicTop}>
<Text style={styles.positionName}>{detail.positionName}</Text>
<View style={styles.statusCol}>
<View style={styles.statusTag}>
<Text style={styles.statusText}>{detail.status}</Text>
</View>
<Text style={styles.viewsText}>浏览{formatThousands(detail.views)}</Text>
</View>
</View>
<Text style={styles.salary}>
{detail.salaryMin}-{detail.salaryMax}K · {detail.salaryMonths}薪
</Text>
<Text style={styles.metaText}>
{detail.companyName} · {detail.industry} · {detail.companySize}
</Text>
<Text style={styles.metaText}>
{detail.companyLocation} · {detail.experience} · {detail.education}
</Text>
<View style={styles.benefits}>
{detail.benefits.map((item) => (
<View key={item} style={styles.benefitTag}>
<Text style={styles.benefitText}>{item}</Text>
</View>
))}
</View>
</View>
{/* 2. 岗位综合评分 */}
{/* 「评分说明」入口已摘除:项目没有评分规则说明页 */}
<SectionTitle title="岗位综合评分" />
<View style={styles.card}>
<View style={styles.scoreRow}>
<RadarChart dimensions={overallScore.dimensions} />
<View style={styles.scoreInfo}>
<View style={styles.scoreNumberRow}>
<Text style={styles.scoreNumber}>{overallScore.totalScore}</Text>
<Text style={styles.scoreUnit}>分</Text>
</View>
<View style={styles.levelTag}>
<Text style={styles.levelTagText}>{overallScore.level}</Text>
</View>
<Text style={styles.scoreRank}>{overallScore.rank}</Text>
</View>
</View>
<Pressable style={styles.compareBtn} onPress={() => router.push('/jobs/compare')} hitSlop={6}>
<Ionicons name="add-circle-outline" size={16} color={colors.primary} />
<Text style={styles.compareText}>加入对比</Text>
</Pressable>
</View>
{/* 3. 真实薪资区间:已摘除 */}
{/* 后端无「按经验段拆分薪资」端点(`/jobs/{id}/salary` 不存在,salary-calculator 域实网 404),
该表无数据来源;岗位薪资已在顶部基础信息卡按 salaryMin-salaryMax 展示,不重复占位。 */}
{/* 4. 员工评价(汇总 → 列表) */}
<SectionTitle title="员工评价" />
{reviewSummary ? <ReviewSummaryCard summary={reviewSummary} /> : null}
{reviews.length === 0 && (
/* 标题是常驻的,没有这一块时下方会是一片空白,看上去像加载坏了 */
<View style={styles.emptyCard}>
<Ionicons name="chatbubble-outline" size={32} color={colors.textHint} />
<Text style={styles.emptyText}>暂无员工评价</Text>
<Text style={styles.emptyHint}>成为第一个分享这家公司真实情况的人</Text>
</View>
)}
{reviews.map((review) => (
<ReviewCard
key={review.id}
review={review}
onEdit={() => openEdit(review)}
onDelete={() => handleDeleteReview(review)}
/>
))}
</ScrollView>
{/* 5. 评价入口(固定底部):已有本人评价时改为编辑,避免用户以为能重复发布 */}
<View style={styles.commentBar}>
<Pressable
style={styles.composeBtn}
onPress={myReview ? () => openEdit(myReview) : openCreate}
>
<Ionicons
name={myReview ? 'create-outline' : 'pencil-outline'}
size={16}
color={colors.white}
/>
<Text style={styles.publishText}>{myReview ? '编辑我的评价' : '写评价'}</Text>
</Pressable>
</View>
<ReviewEditorModal
visible={editorOpen}
initial={editing}
submitting={submitting}
onClose={() => {
setEditorOpen(false);
setEditing(null);
}}
onSubmit={handleSubmitReview}
/>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
safeArea: {
flex: 1,
backgroundColor: colors.pageBg,
},
stateWrap: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
stateText: {
marginTop: 12,
fontSize: 14,
color: colors.textSecondary,
},
retryBtn: {
marginTop: 16,
paddingHorizontal: 24,
paddingVertical: spacing.card,
borderRadius: 18,
backgroundColor: colors.primaryBtn,
},
retryText: {
fontSize: 13,
fontWeight: '600',
color: colors.white,
},
/* ---- 顶部导航 ---- */
header: {
height: 48,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
},
headerTitle: {
fontSize: 17,
fontWeight: '700',
color: colors.textPrimary,
},
/**
* 头部按钮:返回靠左、分享靠右。
* 原先两者共用一条**只写了 `top`、没写 `left`/`right`** 的 absolute 样式,
* 在 `justifyContent: 'center'` 的父容器里落到同一处彼此重叠 —— 返回键与分享键
* 都不在用户预期位置(分享键实测点不到)。absolute 必须成对写清水平锚点。
*/
headerBtn: {
position: 'absolute',
top: 0,
left: 0,
width: 48,
height: 48,
alignItems: 'center',
justifyContent: 'center',
},
headerBtnRight: {
position: 'absolute',
top: 0,
right: 0,
width: 48,
height: 48,
alignItems: 'center',
justifyContent: 'center',
},
scroll: {
flex: 1,
},
content: {
paddingHorizontal: spacing.card,
paddingBottom: 24,
},
card: {
backgroundColor: colors.white,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
},
/* ---- 空态 ---- */
emptyCard: {
backgroundColor: colors.white,
borderRadius: spacing.card,
paddingVertical: spacing.card,
paddingHorizontal: spacing.card,
marginBottom: 8,
alignItems: 'center',
gap: 8,
},
emptyText: {
fontSize: 14,
fontWeight: '600',
color: colors.textSecondary,
},
emptyHint: {
fontSize: 12,
color: colors.textHint,
textAlign: 'center',
},
/* ---- 基本信息 ---- */
basicTop: {
flexDirection: 'row',
alignItems: 'flex-start',
justifyContent: 'space-between',
},
positionName: {
flex: 1,
fontSize: 22,
fontWeight: '700',
color: colors.textPrimary,
},
statusCol: {
alignItems: 'flex-end',
marginLeft: 12,
},
statusTag: {
backgroundColor: colors.successLight,
paddingHorizontal: spacing.card,
paddingVertical: 3,
borderRadius: 10,
},
statusText: {
fontSize: 12,
fontWeight: '600',
color: colors.successText,
},
viewsText: {
marginTop: 6,
fontSize: 12,
color: colors.textHint,
},
salary: {
marginTop: 14,
fontSize: 20,
fontWeight: '700',
color: colors.primary,
},
metaText: {
marginTop: 8,
fontSize: 13,
color: colors.textSecondary,
},
benefits: {
flexDirection: 'row',
flexWrap: 'wrap',
marginTop: 12,
},
benefitTag: {
backgroundColor: colors.primaryLight,
borderWidth: 1,
borderColor: colors.primaryBorder,
borderRadius: 12,
paddingHorizontal: 10,
paddingVertical: 4,
marginRight: 8,
marginBottom: 8,
},
benefitText: {
fontSize: 12,
color: colors.primary,
},
/* ---- 分区标题 ---- */
sectionHeader: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 12,
},
sectionBar: {
width: 4,
height: 16,
borderRadius: 2,
backgroundColor: colors.primary,
marginRight: 8,
},
sectionTitle: {
flex: 1,
fontSize: 17,
fontWeight: '700',
color: colors.textPrimary,
},
sectionLinkBtn: {
paddingLeft: 8,
},
sectionLink: {
fontSize: 13,
color: colors.primary,
},
/* ---- 综合评分 ---- */
scoreRow: {
flexDirection: 'row',
alignItems: 'center',
},
scoreInfo: {
flex: 1,
alignItems: 'flex-start',
marginLeft: 8,
},
scoreNumberRow: {
flexDirection: 'row',
alignItems: 'baseline',
},
scoreNumber: {
fontSize: 40,
fontWeight: '700',
color: colors.primary,
},
scoreUnit: {
fontSize: 14,
fontWeight: '600',
color: colors.primary,
},
levelTag: {
marginTop: 4,
backgroundColor: colors.successLight,
paddingHorizontal: 10,
paddingVertical: 4,
borderRadius: 12,
},
levelTagText: {
fontSize: 13,
fontWeight: '600',
color: colors.successText,
},
scoreRank: {
marginTop: 8,
fontSize: 12,
color: colors.textSecondary,
},
compareBtn: {
flexDirection: 'row',
alignItems: 'center',
alignSelf: 'flex-start',
marginTop: 16,
},
compareText: {
marginLeft: 4,
fontSize: 14,
fontWeight: '600',
color: colors.primary,
},
/* ---- 员工评价 ---- */
reviewHeader: {
flexDirection: 'row',
alignItems: 'center',
},
reviewName: {
fontSize: 14,
fontWeight: '700',
color: colors.textPrimary,
},
reviewRole: {
marginLeft: 8,
fontSize: 12,
color: colors.textSecondary,
},
reviewTime: {
flex: 1,
textAlign: 'right',
fontSize: 12,
color: colors.textHint,
},
reviewContent: {
marginTop: 10,
fontSize: 13,
lineHeight: 20,
color: colors.textPrimary,
},
reviewTags: {
flexDirection: 'row',
flexWrap: 'wrap',
marginTop: 10,
},
reviewTag: {
backgroundColor: colors.pageBg,
borderWidth: 1,
borderColor: colors.border,
borderRadius: 12,
paddingHorizontal: 10,
paddingVertical: 4,
marginRight: 8,
marginBottom: 8,
},
reviewTagText: {
fontSize: 12,
color: colors.textSecondary,
},
/* ---- 评价汇总 ---- */
summaryTop: {
flexDirection: 'row',
alignItems: 'center',
},
summaryScoreCol: {
width: 104,
alignItems: 'center',
gap: 4,
},
summaryScore: {
fontSize: 30,
fontWeight: '700',
color: colors.textPrimary,
},
summaryTotal: {
fontSize: 11,
color: colors.textHint,
},
summaryBars: {
flex: 1,
marginLeft: 12,
gap: 4,
},
summaryBarRow: {
flexDirection: 'row',
alignItems: 'center',
},
summaryBarLabel: {
width: 30,
fontSize: 11,
color: colors.textHint,
},
summaryBarTrack: {
flex: 1,
height: 6,
borderRadius: 3,
backgroundColor: colors.pageBg,
overflow: 'hidden',
},
summaryBarFill: {
height: 6,
borderRadius: 3,
backgroundColor: STAR_COLOR,
},
summaryBarCount: {
width: 26,
textAlign: 'right',
fontSize: 11,
color: colors.textHint,
},
/* ---- 评价卡 ---- */
reviewRatingRow: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 8,
gap: 4,
},
reviewRatingText: {
fontSize: 12,
fontWeight: '600',
color: STAR_COLOR,
},
dimGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
marginTop: 10,
gap: spacing.card,
},
dimCell: {
minWidth: 72,
paddingHorizontal: 8,
paddingVertical: 6,
borderRadius: spacing.card,
backgroundColor: colors.pageBg,
},
dimLabel: {
fontSize: 11,
color: colors.textHint,
},
dimValue: {
marginTop: 2,
fontSize: 14,
fontWeight: '700',
color: colors.textPrimary,
},
reviewActions: {
flexDirection: 'row',
justifyContent: 'flex-end',
marginTop: 12,
paddingTop: 10,
borderTopWidth: 1,
borderTopColor: colors.borderLight,
gap: 16,
},
reviewActionBtn: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
},
reviewActionText: {
fontSize: 12,
fontWeight: '600',
color: colors.primaryText,
},
reviewActionDanger: {
color: colors.dangerText,
},
/* ---- 底部评价入口 ---- */
commentBar: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: spacing.framework,
paddingVertical: 10,
backgroundColor: colors.white,
borderTopWidth: 1,
borderTopColor: colors.borderLight,
},
composeBtn: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 6,
height: 40,
borderRadius: 20,
backgroundColor: colors.primaryBtn,
},
publishText: {
fontSize: 14,
fontWeight: '600',
color: colors.white,
},
/* ---- 评价编辑器弹窗 ---- */
modalMask: {
flex: 1,
backgroundColor: colors.scrim,
justifyContent: 'flex-end',
},
modalSheet: {
maxHeight: '86%',
backgroundColor: colors.white,
borderTopLeftRadius: spacing.framework,
borderTopRightRadius: spacing.framework,
},
modalHeader: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: spacing.framework,
paddingVertical: 14,
borderBottomWidth: 1,
borderBottomColor: colors.borderLight,
},
modalTitle: {
fontSize: 16,
fontWeight: '700',
color: colors.textPrimary,
},
modalBody: {
paddingHorizontal: spacing.framework,
paddingTop: 12,
},
fieldLabel: {
marginTop: 14,
marginBottom: 8,
fontSize: 13,
fontWeight: '600',
color: colors.textPrimary,
},
ratingRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 12,
},
ratingHint: {
fontSize: 13,
color: colors.textSecondary,
},
dimEditor: {
gap: 6,
},
dimEditorRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
dimEditorLabel: {
fontSize: 13,
color: colors.textSecondary,
},
tagPicker: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: spacing.card,
},
tagChip: {
paddingHorizontal: 10,
paddingVertical: 5,
borderRadius: spacing.card,
borderWidth: 1,
borderColor: colors.border,
backgroundColor: colors.white,
},
tagChipActive: {
borderColor: colors.primary,
backgroundColor: colors.primaryLight,
},
tagChipText: {
fontSize: 12,
color: colors.textSecondary,
},
tagChipTextActive: {
color: colors.primaryText,
fontWeight: '600',
},
modalInput: {
minHeight: 96,
borderRadius: spacing.card,
borderWidth: 1,
borderColor: colors.border,
backgroundColor: colors.pageBg,
paddingHorizontal: spacing.card,
paddingVertical: spacing.card,
fontSize: 13,
lineHeight: 20,
color: colors.textPrimary,
textAlignVertical: 'top',
},
counterText: {
marginTop: 6,
textAlign: 'right',
fontSize: 11,
color: colors.textHint,
},
modalFooter: {
paddingHorizontal: spacing.framework,
paddingTop: 10,
paddingBottom: 24,
},
submitBtn: {
height: 44,
borderRadius: 22,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: colors.primaryBtn,
},
submitBtnDisabled: {
opacity: 0.6,
},
submitText: {
fontSize: 15,
fontWeight: '700',
color: colors.white,
},
});